diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js index f462f8356..694d0a3ac 100644 --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -1,406 +1,411 @@ // @flow import html from 'common-tags/lib/html/index.js'; import { detect as detectBrowser } from 'detect-browser'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; import { promisify } from 'util'; import stores from 'lib/facts/stores.js'; import getTitle from 'web/title/get-title.js'; import { waitForStream } from '../utils/json-stream.js'; import { getAndAssertKeyserverURLFacts, getAppURLFactsFromRequestURL, getWebAppURLFacts, } from '../utils/urls.js'; // eslint-disable-next-line react/no-deprecated const { renderToNodeStream } = ReactDOMServer; const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600&display=swap'; const localFontsURL = 'fonts/local-fonts.css'; async function getFontsURL() { try { await access(localFontsURL); return localFontsURL; } catch { return googleFontsURL; } } type AssetInfo = { +jsURL: string, +fontsURL: string, +cssInclude: string, +olmFilename: string, +commQueryExecutorFilename: string, +opaqueURL: string, +backupClientFilename: string, + +webworkersOpaqueFilename: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontsURL = await getFontsURL(); assetInfo = { jsURL: 'http://localhost:8080/dev.build.js', fontsURL, cssInclude: '', olmFilename: '', commQueryExecutorFilename: '', opaqueURL: 'http://localhost:8080/opaque-ke.wasm', backupClientFilename: '', + webworkersOpaqueFilename: '', }; return assetInfo; } try { const manifestString = await readFile('../web/dist/manifest.json', 'utf8'); const manifest = JSON.parse(manifestString); const webworkersManifestString = await readFile( '../web/dist/webworkers/manifest.json', 'utf8', ); const webworkersManifest = JSON.parse(webworkersManifestString); assetInfo = { jsURL: `compiled/${manifest['browser.js']}`, fontsURL: googleFontsURL, cssInclude: html` `, olmFilename: manifest['olm.wasm'], commQueryExecutorFilename: webworkersManifest['comm_query_executor.wasm'], opaqueURL: `compiled/${manifest['comm_opaque2_wasm_bg.wasm']}`, backupClientFilename: webworkersManifest['backup-client-wasm_bg.wasm'], + webworkersOpaqueFilename: webworkersManifest['comm_opaque2_wasm_bg.wasm'], }; return assetInfo; } catch { throw new Error( 'Could not load manifest.json for web build. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } let webpackCompiledRootComponent: ?React.ComponentType<{}> = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe web/dist doesn't always exist const webpackBuild = await import('web/dist/app.build.cjs'); webpackCompiledRootComponent = webpackBuild.app.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load app.build.cjs. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } function stripLastSlash(input: string): string { return input.replace(/\/$/, ''); } async function websiteResponder(req: $Request, res: $Response): Promise { const { basePath } = getAppURLFactsFromRequestURL(req.originalUrl); const baseURL = stripLastSlash(basePath); const keyserverURLFacts = getAndAssertKeyserverURLFacts(); const keyserverURL = `${keyserverURLFacts.baseDomain}${stripLastSlash( keyserverURLFacts.basePath, )}`; const loadingPromise = getWebpackCompiledRootComponentForSSR(); const assetInfoPromise = getAssetInfo(); const { jsURL, fontsURL, cssInclude, olmFilename, opaqueURL, commQueryExecutorFilename, backupClientFilename, + webworkersOpaqueFilename, } = await assetInfoPromise; // prettier-ignore res.write(html` ${getTitle(0)} ${cssInclude}
`); const Loading = await loadingPromise; const reactStream = renderToNodeStream(); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); res.end(html`
`); } const inviteSecretRegex = /^[a-z0-9]+$/i; // On native, if this responder is called, it means that the app isn't // installed. async function inviteResponder(req: $Request, res: $Response): Promise { const { secret } = req.params; const userAgent = req.get('User-Agent'); const detectionResult = detectBrowser(userAgent); if (detectionResult.os === 'Android OS') { const isSecretValid = inviteSecretRegex.test(secret); const referrer = isSecretValid ? `&referrer=${encodeURIComponent(`utm_source=invite/${secret}`)}` : ''; const redirectUrl = `${stores.googlePlayUrl}${referrer}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } else if (detectionResult.os !== 'iOS') { const urlFacts = getWebAppURLFacts(); const baseDomain = urlFacts?.baseDomain ?? ''; const basePath = urlFacts?.basePath ?? '/'; const redirectUrl = `${baseDomain}${basePath}handle/invite/${secret}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } const fontsURL = await getFontsURL(); res.end(html` Comm

Comm

To join this community, download the Comm app and reopen this invite link

Download Comm Invite Link
Visit Comm’s website arrow up right `); } export { websiteResponder, inviteResponder }; diff --git a/web/shared-worker/utils/constants.js b/web/shared-worker/utils/constants.js index b82bb9ad6..7bd3027cd 100644 --- a/web/shared-worker/utils/constants.js +++ b/web/shared-worker/utils/constants.js @@ -1,57 +1,69 @@ // @flow import localforage from 'localforage'; export const SQLITE_CONTENT = 'sqliteFileContent'; export const SQLITE_ENCRYPTION_KEY = 'encryptionKey'; export const CURRENT_USER_ID_KEY = 'current_user_id'; export const DATABASE_WORKER_PATH = 'worker/database'; export const WORKERS_MODULES_DIR_PATH = '/compiled/webworkers'; export const DEFAULT_COMM_QUERY_EXECUTOR_FILENAME = 'comm_query_executor.wasm'; export const DEFAULT_BACKUP_CLIENT_FILENAME = 'backup-client-wasm_bg.wasm'; export const DEFAULT_OLM_FILENAME = 'olm.wasm'; +export const DEFAULT_WEBWORKERS_OPAQUE_FILENAME = 'comm_opaque2_wasm_bg.wasm'; + export const COMM_SQLITE_DATABASE_PATH = 'comm.sqlite'; export const COMM_SQLITE_BACKUP_RESTORE_DATABASE_PATH = 'comm_backup_restore.sqlite'; export const NOTIFICATIONS_OLM_DATA_CONTENT = 'notificationsOlmDataContent'; export const NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY = 'notificationsOlmDataEncryptionKey'; export const DB_SUPPORTED_OS: $ReadOnlyArray = [ 'Windows 10', 'Linux', 'Mac OS', ]; export const DB_SUPPORTED_BROWSERS: $ReadOnlyArray = [ 'edge', 'edge-chromium', 'chrome', 'firefox', 'opera', 'safari', ]; export const localforageConfig: PartialConfig = { driver: localforage.INDEXEDDB, name: 'comm', storeName: 'commStorage', description: 'Comm encrypted database storage', version: '1.0', }; declare var baseURL: string; declare var olmFilename: string; export function getOlmWasmPath(): string { const origin = window.location.origin; const olmWasmDirPath = `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`; const olmWasmFilename = olmFilename ? olmFilename : DEFAULT_OLM_FILENAME; return `${olmWasmDirPath}/${olmWasmFilename}`; } + +declare var webworkersOpaqueFilename: string; +export function getOpaqueWasmPath(): string { + const origin = window.location.origin; + const opaqueWasmDirPath = `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`; + const opaqueWasmFilename = webworkersOpaqueFilename + ? webworkersOpaqueFilename + : DEFAULT_WEBWORKERS_OPAQUE_FILENAME; + return `${opaqueWasmDirPath}/${opaqueWasmFilename}`; +} diff --git a/web/shared-worker/worker/identity-client.js b/web/shared-worker/worker/identity-client.js new file mode 100644 index 000000000..c1b67e596 --- /dev/null +++ b/web/shared-worker/worker/identity-client.js @@ -0,0 +1,36 @@ +// @flow + +import { getDeviceKeyUpload } from './worker-crypto.js'; +import { IdentityServiceClientWrapper } from '../../grpc/identity-service-client-wrapper.js'; +import { workerRequestMessageTypes } from '../../types/worker-types.js'; +import type { + WorkerResponseMessage, + WorkerRequestMessage, +} from '../../types/worker-types.js'; +import type { EmscriptenModule } from '../types/module.js'; +import type { SQLiteQueryExecutor } from '../types/sqlite-query-executor.js'; + +let identityClient: ?IdentityServiceClientWrapper = null; + +async function processAppIdentityClientRequest( + sqliteQueryExecutor: SQLiteQueryExecutor, + dbModule: EmscriptenModule, + message: WorkerRequestMessage, +): Promise { + if ( + message.type === workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT + ) { + identityClient = new IdentityServiceClientWrapper( + message.platformDetails, + message.opaqueWasmPath, + message.authLayer, + async () => getDeviceKeyUpload(), + ); + } +} + +function getIdentityClient(): ?IdentityServiceClientWrapper { + return identityClient; +} + +export { processAppIdentityClientRequest, getIdentityClient }; diff --git a/web/shared-worker/worker/shared-worker.js b/web/shared-worker/worker/shared-worker.js index 4f51ff21d..cdd6a4954 100644 --- a/web/shared-worker/worker/shared-worker.js +++ b/web/shared-worker/worker/shared-worker.js @@ -1,316 +1,332 @@ // @flow import localforage from 'localforage'; import { restoreBackup } from './backup.js'; +import { processAppIdentityClientRequest } from './identity-client.js'; import { getClientStoreFromQueryExecutor, processDBStoreOperations, } from './process-operations.js'; import { clearCryptoStore, processAppOlmApiRequest } from './worker-crypto.js'; import { getDBModule, getSQLiteQueryExecutor, setDBModule, setSQLiteQueryExecutor, } from './worker-database.js'; import initBackupClientModule from '../../backup-client-wasm/wasm/backup-client-wasm.js'; import { decryptData, encryptData, generateCryptoKey, importJWKKey, type EncryptedData, } from '../../crypto/aes-gcm-crypto-utils.js'; import { type SharedWorkerMessageEvent, type WorkerRequestMessage, type WorkerResponseMessage, workerRequestMessageTypes, workerResponseMessageTypes, type WorkerRequestProxyMessage, workerWriteRequests, workerOlmAPIRequests, } from '../../types/worker-types.js'; +import { workerIdentityClientRequests } from '../../types/worker-types.js'; import { getDatabaseModule } from '../db-module.js'; import { COMM_SQLITE_DATABASE_PATH, CURRENT_USER_ID_KEY, localforageConfig, SQLITE_CONTENT, SQLITE_ENCRYPTION_KEY, DEFAULT_BACKUP_CLIENT_FILENAME, } from '../utils/constants.js'; import { clearSensitiveData, exportDatabaseContent, importDatabaseContent, } from '../utils/db-utils.js'; localforage.config(localforageConfig); let encryptionKey: ?CryptoKey = null; let persistNeeded: boolean = false; let persistInProgress: boolean = false; async function initDatabase( webworkerModulesFilePath: string, commQueryExecutorFilename: ?string, encryptionKeyJWK?: ?SubtleCrypto$JsonWebKey, ) { const dbModule = getDBModule(); const sqliteQueryExecutor = getSQLiteQueryExecutor(); if (!!dbModule && !!sqliteQueryExecutor) { console.log('Database already initialized'); return; } const newModule = dbModule ? dbModule : getDatabaseModule(commQueryExecutorFilename, webworkerModulesFilePath); if (!dbModule) { setDBModule(newModule); } if (encryptionKeyJWK) { encryptionKey = await importJWKKey(encryptionKeyJWK); } else { encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY); if (!encryptionKey) { const cryptoKey = await generateCryptoKey({ extractable: false }); await localforage.setItem(SQLITE_ENCRYPTION_KEY, cryptoKey); } } const encryptedContent = await localforage.getItem(SQLITE_CONTENT); let dbContent = null; try { if (encryptionKey && encryptedContent) { dbContent = await decryptData(encryptedContent, encryptionKey); } } catch (e) { console.error('Error while decrypting content, clearing database content'); await localforage.removeItem(SQLITE_CONTENT); } if (dbContent) { importDatabaseContent(dbContent, newModule, COMM_SQLITE_DATABASE_PATH); console.info( 'Database exists and is properly encrypted, using persisted data', ); } else { console.info('Creating fresh database'); } setSQLiteQueryExecutor( new newModule.SQLiteQueryExecutor(COMM_SQLITE_DATABASE_PATH), ); } async function initBackupClient( webworkerModulesFilePath: string, backupClientFilename: ?string, ) { let modulePath; if (backupClientFilename) { modulePath = `${webworkerModulesFilePath}/${backupClientFilename}`; } else { modulePath = `${webworkerModulesFilePath}/${DEFAULT_BACKUP_CLIENT_FILENAME}`; } await initBackupClientModule(modulePath); } async function persist() { persistInProgress = true; const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { persistInProgress = false; throw new Error( 'Database not initialized while persisting database content', ); } if (!encryptionKey) { encryptionKey = await localforage.getItem(SQLITE_ENCRYPTION_KEY); } while (persistNeeded) { persistNeeded = false; const dbData = exportDatabaseContent(dbModule, COMM_SQLITE_DATABASE_PATH); if (!encryptionKey) { persistInProgress = false; throw new Error('Encryption key is missing'); } const encryptedData = await encryptData(dbData, encryptionKey); await localforage.setItem(SQLITE_CONTENT, encryptedData); } persistInProgress = false; } async function processAppRequest( message: WorkerRequestMessage, ): Promise { // non-database operations if (message.type === workerRequestMessageTypes.PING) { return { type: workerResponseMessageTypes.PONG, text: 'PONG', }; } else if ( message.type === workerRequestMessageTypes.GENERATE_DATABASE_ENCRYPTION_KEY ) { const cryptoKey = await generateCryptoKey({ extractable: false }); await localforage.setItem(SQLITE_ENCRYPTION_KEY, cryptoKey); return undefined; } const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); // database operations if (message.type === workerRequestMessageTypes.INIT) { const promises = [ initDatabase( message.webworkerModulesFilePath, message.commQueryExecutorFilename, message.encryptionKey, ), ]; if (message.backupClientFilename !== undefined) { promises.push( initBackupClient( message.webworkerModulesFilePath, message.backupClientFilename, ), ); } await Promise.all(promises); return undefined; } else if (message.type === workerRequestMessageTypes.CLEAR_SENSITIVE_DATA) { clearCryptoStore(); encryptionKey = null; await localforage.clear(); if (dbModule && sqliteQueryExecutor) { clearSensitiveData( dbModule, COMM_SQLITE_DATABASE_PATH, sqliteQueryExecutor, ); } setSQLiteQueryExecutor(null); return undefined; } if (!sqliteQueryExecutor) { throw new Error( `Database not initialized, unable to process request type: ${message.type}`, ); } // read-only operations if (message.type === workerRequestMessageTypes.GET_CLIENT_STORE) { return { type: workerResponseMessageTypes.CLIENT_STORE, store: getClientStoreFromQueryExecutor(sqliteQueryExecutor), }; } else if (message.type === workerRequestMessageTypes.GET_CURRENT_USER_ID) { return { type: workerResponseMessageTypes.GET_CURRENT_USER_ID, userID: sqliteQueryExecutor.getMetadata(CURRENT_USER_ID_KEY), }; } else if ( message.type === workerRequestMessageTypes.GET_PERSIST_STORAGE_ITEM ) { return { type: workerResponseMessageTypes.GET_PERSIST_STORAGE_ITEM, item: sqliteQueryExecutor.getPersistStorageItem(message.key), }; } // write operations const isOlmAPIRequest = workerOlmAPIRequests.includes(message.type); - if (!workerWriteRequests.includes(message.type) && !isOlmAPIRequest) { + const isIdentityClientRequest = workerIdentityClientRequests.includes( + message.type, + ); + if ( + !workerWriteRequests.includes(message.type) && + !isOlmAPIRequest && + !isIdentityClientRequest + ) { throw new Error(`Request type ${message.type} not supported`); } if (!sqliteQueryExecutor || !dbModule) { throw new Error( `Database not initialized, unable to process request type: ${message.type}`, ); } + let result; if (isOlmAPIRequest) { await processAppOlmApiRequest(message); + } else if (isIdentityClientRequest) { + result = await processAppIdentityClientRequest( + sqliteQueryExecutor, + dbModule, + message, + ); } else if ( message.type === workerRequestMessageTypes.PROCESS_STORE_OPERATIONS ) { processDBStoreOperations( sqliteQueryExecutor, message.storeOperations, dbModule, ); } else if (message.type === workerRequestMessageTypes.SET_CURRENT_USER_ID) { sqliteQueryExecutor.setMetadata(CURRENT_USER_ID_KEY, message.userID); } else if ( message.type === workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM ) { sqliteQueryExecutor.setPersistStorageItem(message.key, message.item); } else if ( message.type === workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM ) { sqliteQueryExecutor.removePersistStorageItem(message.key); } else if (message.type === workerRequestMessageTypes.BACKUP_RESTORE) { await restoreBackup( sqliteQueryExecutor, dbModule, message.authMetadata, message.backupID, message.backupDataKey, message.backupLogDataKey, ); } persistNeeded = true; if (!persistInProgress) { void persist(); } - return undefined; + return result; } function connectHandler(event: SharedWorkerMessageEvent) { if (!event.ports.length) { return; } const port: MessagePort = event.ports[0]; console.log('Web database worker alive!'); port.onmessage = async function (messageEvent: MessageEvent) { const data: WorkerRequestProxyMessage = (messageEvent.data: any); const { id, message } = data; if (!id) { port.postMessage({ error: 'Request without identifier', }); } try { const result = await processAppRequest(message); port.postMessage({ id, message: result, }); } catch (e) { port.postMessage({ id, error: e.message, }); } }; } self.addEventListener('connect', connectHandler); diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js index 2ca467a86..081a806c8 100644 --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,169 +1,230 @@ // @flow import olm from '@commapp/olm'; import uuid from 'uuid'; -import type { CryptoStore, PickledOLMAccount } from 'lib/types/crypto-types.js'; +import type { + CryptoStore, + PickledOLMAccount, + IdentityKeysBlob, + SignedIdentityKeysBlob, +} from 'lib/types/crypto-types.js'; +import type { IdentityDeviceKeyUpload } from 'lib/types/identity-service-types.js'; +import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; import { getDBModule, getSQLiteQueryExecutor } from './worker-database.js'; import { type WorkerRequestMessage, type WorkerResponseMessage, workerRequestMessageTypes, } from '../../types/worker-types.js'; type WorkerCryptoStore = { +contentAccountPickleKey: string, +contentAccount: olm.Account, +notificationAccountPickleKey: string, +notificationAccount: olm.Account, }; let cryptoStore: ?WorkerCryptoStore = null; function clearCryptoStore() { cryptoStore = null; } function persistCryptoStore() { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error( "Couldn't persist crypto store because database is not initialized", ); } if (!cryptoStore) { throw new Error("Couldn't persist crypto store because it doesn't exist"); } const { contentAccountPickleKey, contentAccount, notificationAccountPickleKey, notificationAccount, } = cryptoStore; const pickledContentAccount: PickledOLMAccount = { picklingKey: contentAccountPickleKey, pickledAccount: contentAccount.pickle(contentAccountPickleKey), }; const pickledNotificationAccount: PickledOLMAccount = { picklingKey: notificationAccountPickleKey, pickledAccount: notificationAccount.pickle(notificationAccountPickleKey), }; try { sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getContentAccountID(), JSON.stringify(pickledContentAccount), ); sqliteQueryExecutor.storeOlmPersistAccount( sqliteQueryExecutor.getNotifsAccountID(), JSON.stringify(pickledNotificationAccount), ); } catch (err) { throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } } function getOrCreateOlmAccount(accountIDInDB: number): { +picklingKey: string, +account: olm.Account, } { const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { throw new Error('Database not initialized'); } const account = new olm.Account(); let picklingKey; let accountDBString; try { accountDBString = sqliteQueryExecutor.getOlmPersistAccountDataWeb(accountIDInDB); } catch (err) { throw new Error(getProcessingStoreOpsExceptionMessage(err, dbModule)); } if (accountDBString.isNull) { picklingKey = uuid.v4(); account.create(); } else { const dbAccount: PickledOLMAccount = JSON.parse(accountDBString.value); picklingKey = dbAccount.picklingKey; account.unpickle(picklingKey, dbAccount.pickledAccount); } return { picklingKey, account }; } function unpickleInitialCryptoStoreAccount( account: PickledOLMAccount, ): olm.Account { const { picklingKey, pickledAccount } = account; const olmAccount = new olm.Account(); olmAccount.unpickle(picklingKey, pickledAccount); return olmAccount; } async function initializeCryptoAccount( olmWasmPath: string, initialCryptoStore: ?CryptoStore, ) { const sqliteQueryExecutor = getSQLiteQueryExecutor(); if (!sqliteQueryExecutor) { throw new Error('Database not initialized'); } await olm.init({ locateFile: () => olmWasmPath }); if (initialCryptoStore) { cryptoStore = { contentAccountPickleKey: initialCryptoStore.primaryAccount.picklingKey, contentAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.primaryAccount, ), notificationAccountPickleKey: initialCryptoStore.notificationAccount.picklingKey, notificationAccount: unpickleInitialCryptoStoreAccount( initialCryptoStore.notificationAccount, ), }; persistCryptoStore(); return; } const contentAccountResult = getOrCreateOlmAccount( sqliteQueryExecutor.getContentAccountID(), ); const notificationAccountResult = getOrCreateOlmAccount( sqliteQueryExecutor.getNotifsAccountID(), ); cryptoStore = { contentAccountPickleKey: contentAccountResult.picklingKey, contentAccount: contentAccountResult.account, notificationAccountPickleKey: notificationAccountResult.picklingKey, notificationAccount: notificationAccountResult.account, }; persistCryptoStore(); } async function processAppOlmApiRequest( message: WorkerRequestMessage, ): Promise { if (message.type === workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT) { await initializeCryptoAccount( message.olmWasmPath, message.initialCryptoStore, ); } } -export { clearCryptoStore, processAppOlmApiRequest }; +function getSignedIdentityKeysBlob(): SignedIdentityKeysBlob { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + + const { contentAccount, notificationAccount } = cryptoStore; + + const identityKeysBlob: IdentityKeysBlob = { + notificationIdentityPublicKeys: JSON.parse( + notificationAccount.identity_keys(), + ), + primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()), + }; + + const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); + const signedIdentityKeysBlob: SignedIdentityKeysBlob = { + payload: payloadToBeSigned, + signature: contentAccount.sign(payloadToBeSigned), + }; + + return signedIdentityKeysBlob; +} + +function getDeviceKeyUpload(): IdentityDeviceKeyUpload { + if (!cryptoStore) { + throw new Error('Crypto account not initialized'); + } + const { contentAccount, notificationAccount } = cryptoStore; + + const signedIdentityKeysBlob = getSignedIdentityKeysBlob(); + + const primaryAccountKeysSet = retrieveAccountKeysSet(contentAccount); + const notificationAccountKeysSet = + retrieveAccountKeysSet(notificationAccount); + + persistCryptoStore(); + + return { + keyPayload: signedIdentityKeysBlob.payload, + keyPayloadSignature: signedIdentityKeysBlob.signature, + contentPrekey: primaryAccountKeysSet.prekey, + contentPrekeySignature: primaryAccountKeysSet.prekeySignature, + notifPrekey: notificationAccountKeysSet.prekey, + notifPrekeySignature: notificationAccountKeysSet.prekeySignature, + contentOneTimeKeys: primaryAccountKeysSet.oneTimeKeys, + notifOneTimeKeys: notificationAccountKeysSet.oneTimeKeys, + }; +} + +export { + clearCryptoStore, + processAppOlmApiRequest, + getSignedIdentityKeysBlob, + getDeviceKeyUpload, +}; diff --git a/web/types/worker-types.js b/web/types/worker-types.js index db97fdb81..2d3bd7d01 100644 --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -1,173 +1,188 @@ // @flow import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import type { CryptoStore } from 'lib/types/crypto-types.js'; +import type { PlatformDetails } from 'lib/types/device-types.js'; +import type { IdentityServiceAuthLayer } from 'lib/types/identity-service-types.js'; import type { ClientDBStore, ClientDBStoreOperations, } from 'lib/types/store-ops-types.js'; // The types of messages sent from app to worker export const workerRequestMessageTypes = Object.freeze({ PING: 0, INIT: 1, GENERATE_DATABASE_ENCRYPTION_KEY: 2, PROCESS_STORE_OPERATIONS: 3, GET_CLIENT_STORE: 4, SET_CURRENT_USER_ID: 5, GET_CURRENT_USER_ID: 6, GET_PERSIST_STORAGE_ITEM: 7, SET_PERSIST_STORAGE_ITEM: 8, REMOVE_PERSIST_STORAGE_ITEM: 9, CLEAR_SENSITIVE_DATA: 10, BACKUP_RESTORE: 11, INITIALIZE_CRYPTO_ACCOUNT: 12, + CREATE_IDENTITY_SERVICE_CLIENT: 13, }); export const workerWriteRequests: $ReadOnlyArray = [ workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, workerRequestMessageTypes.SET_CURRENT_USER_ID, workerRequestMessageTypes.SET_PERSIST_STORAGE_ITEM, workerRequestMessageTypes.REMOVE_PERSIST_STORAGE_ITEM, workerRequestMessageTypes.BACKUP_RESTORE, workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, ]; export const workerOlmAPIRequests: $ReadOnlyArray = [ workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, ]; +export const workerIdentityClientRequests: $ReadOnlyArray = [ + workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT, +]; + export type PingWorkerRequestMessage = { +type: 0, +text: string, }; export type InitWorkerRequestMessage = { +type: 1, +webworkerModulesFilePath: string, +commQueryExecutorFilename: ?string, +encryptionKey?: ?SubtleCrypto$JsonWebKey, +backupClientFilename?: ?string, }; export type GenerateDatabaseEncryptionKeyRequestMessage = { +type: 2, }; export type ProcessStoreOperationsRequestMessage = { +type: 3, +storeOperations: ClientDBStoreOperations, }; export type GetClientStoreRequestMessage = { +type: 4, }; export type SetCurrentUserIDRequestMessage = { +type: 5, +userID: string, }; export type GetCurrentUserIDRequestMessage = { +type: 6, }; export type GetPersistStorageItemRequestMessage = { +type: 7, +key: string, }; export type SetPersistStorageItemRequestMessage = { +type: 8, +key: string, +item: string, }; export type RemovePersistStorageItemRequestMessage = { +type: 9, +key: string, }; export type ClearSensitiveDataRequestMessage = { +type: 10, }; export type BackupRestoreRequestMessage = { +type: 11, +authMetadata: AuthMetadata, +backupID: string, +backupDataKey: string, +backupLogDataKey: string, }; export type InitializeCryptoAccountRequestMessage = { +type: 12, +olmWasmPath: string, +initialCryptoStore?: CryptoStore, }; +export type CreateIdentityServiceClientRequestMessage = { + +type: 13, + +opaqueWasmPath: string, + +platformDetails: PlatformDetails, + +authLayer: ?IdentityServiceAuthLayer, +}; + export type WorkerRequestMessage = | PingWorkerRequestMessage | InitWorkerRequestMessage | GenerateDatabaseEncryptionKeyRequestMessage | ProcessStoreOperationsRequestMessage | GetClientStoreRequestMessage | SetCurrentUserIDRequestMessage | GetCurrentUserIDRequestMessage | GetPersistStorageItemRequestMessage | SetPersistStorageItemRequestMessage | RemovePersistStorageItemRequestMessage | ClearSensitiveDataRequestMessage | BackupRestoreRequestMessage - | InitializeCryptoAccountRequestMessage; + | InitializeCryptoAccountRequestMessage + | CreateIdentityServiceClientRequestMessage; export type WorkerRequestProxyMessage = { +id: number, +message: WorkerRequestMessage, }; // The types of messages sent from worker to app export const workerResponseMessageTypes = Object.freeze({ PONG: 0, CLIENT_STORE: 1, GET_CURRENT_USER_ID: 2, GET_PERSIST_STORAGE_ITEM: 3, }); export type PongWorkerResponseMessage = { +type: 0, +text: string, }; export type ClientStoreResponseMessage = { +type: 1, +store: ClientDBStore, }; export type GetCurrentUserIDResponseMessage = { +type: 2, +userID: ?string, }; export type GetPersistStorageItemResponseMessage = { +type: 3, +item: string, }; export type WorkerResponseMessage = | PongWorkerResponseMessage | ClientStoreResponseMessage | GetCurrentUserIDResponseMessage | GetPersistStorageItemResponseMessage; export type WorkerResponseProxyMessage = { +id?: number, +message?: WorkerResponseMessage, +error?: string, }; // SharedWorker types export type SharedWorkerMessageEvent = MessageEvent & { +ports: $ReadOnlyArray, ... }; diff --git a/web/webpack.config.cjs b/web/webpack.config.cjs index e0a490a80..517b48215 100644 --- a/web/webpack.config.cjs +++ b/web/webpack.config.cjs @@ -1,257 +1,282 @@ const CopyPlugin = require('copy-webpack-plugin'); const path = require('path'); const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const { createProdBrowserConfig, createDevBrowserConfig, createNodeServerRenderingConfig, createWebWorkersConfig, } = require('lib/webpack/shared.cjs'); const babelConfig = require('./.babelrc.cjs'); async function getConfig(configName) { const { getCommConfig } = await import( 'keyserver/dist/lib/utils/comm-config.js' ); return await getCommConfig(configName); } const baseBrowserConfig = { entry: { browser: ['./script.js'], }, output: { filename: 'prod.[contenthash:12].build.js', path: path.join(__dirname, 'dist'), }, resolve: { alias: { '../images': path.resolve('../keyserver/images'), }, fallback: { crypto: false, fs: false, path: false, }, }, }; const baseDevBrowserConfig = { ...baseBrowserConfig, output: { ...baseBrowserConfig.output, filename: 'dev.build.js', pathinfo: true, publicPath: 'http://localhost:8080/', }, devServer: { port: 8080, headers: { 'Access-Control-Allow-Origin': '*' }, allowedHosts: ['all'], host: '0.0.0.0', static: { directory: path.join(__dirname, 'dist'), }, }, plugins: [ new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join(__dirname, 'dist'), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/opaque-ke-wasm' + '/pkg/comm_opaque2_wasm_bg.wasm', to: path.join(__dirname, 'dist', 'opaque-ke.wasm'), }, ], }), ], }; const baseProdBrowserConfig = { ...baseBrowserConfig, plugins: [ new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join(__dirname, 'dist', 'olm.[contenthash:12].wasm'), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/opaque-ke-wasm' + '/pkg/comm_opaque2_wasm_bg.wasm', to: path.join(__dirname, 'dist', 'opaque-ke.[contenthash:12].wasm'), }, ], }), new WebpackManifestPlugin({ publicPath: '', }), ], }; const baseNodeServerRenderingConfig = { externals: ['react', 'react-dom', 'react-redux'], entry: { keyserver: ['./loading.react.js'], }, output: { filename: 'app.build.cjs', library: 'app', libraryTarget: 'commonjs2', path: path.join(__dirname, 'dist'), }, }; const baseWebWorkersConfig = { entry: { pushNotif: './push-notif/service-worker.js', database: './shared-worker/worker/shared-worker.js', }, output: { filename: '[name].build.js', path: path.join(__dirname, 'dist', 'webworkers'), }, resolve: { fallback: { crypto: false, fs: false, path: false, }, }, }; const devWebWorkersPlugins = [ new CopyPlugin({ patterns: [ { from: 'shared-worker/_generated/comm_query_executor.wasm', to: path.join(__dirname, 'dist', 'webworkers'), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join(__dirname, 'dist', 'webworkers'), }, ], }), new CopyPlugin({ patterns: [ { from: 'backup-client-wasm/wasm/backup-client-wasm_bg.wasm', to: path.join(__dirname, 'dist', 'webworkers'), }, ], }), + new CopyPlugin({ + patterns: [ + { + from: + 'node_modules/@commapp/opaque-ke-wasm' + + '/pkg/comm_opaque2_wasm_bg.wasm', + to: path.join(__dirname, 'dist', 'webworkers'), + }, + ], + }), ]; const prodWebWorkersPlugins = [ new CopyPlugin({ patterns: [ { from: 'shared-worker/_generated/comm_query_executor.wasm', to: path.join( __dirname, 'dist', 'webworkers', 'comm_query_executor.[contenthash:12].wasm', ), }, ], }), new CopyPlugin({ patterns: [ { from: 'node_modules/@commapp/olm/olm.wasm', to: path.join( __dirname, 'dist', 'webworkers', 'olm.[contenthash:12].wasm', ), }, ], }), new CopyPlugin({ patterns: [ { from: 'backup-client-wasm/wasm/backup-client-wasm_bg.wasm', to: path.join( __dirname, 'dist', 'webworkers', 'backup-client.[contenthash:12].wasm', ), }, ], }), + new CopyPlugin({ + patterns: [ + { + from: + 'node_modules/@commapp/opaque-ke-wasm' + + '/pkg/comm_opaque2_wasm_bg.wasm', + to: path.join( + __dirname, + 'dist', + 'webworkers', + 'opaque-ke.[contenthash:12].wasm', + ), + }, + ], + }), new WebpackManifestPlugin({ publicPath: '', }), ]; module.exports = async function (env) { const identityServiceConfig = await getConfig({ folder: 'secrets', name: 'identity_service_config', }); const identitySocketAddr = JSON.stringify( identityServiceConfig?.identitySocketAddr, ); const authoritativeKeyserverIDConfig = await getConfig({ folder: 'facts', name: 'authoritative_keyserver', }); const authoritativeKeyserverID = JSON.stringify( authoritativeKeyserverIDConfig?.authoritativeKeyserverID, ); const envVars = { IDENTITY_SOCKET_ADDR: identitySocketAddr, AUTHORITATIVE_KEYSERVER_ID: authoritativeKeyserverID, }; const browserConfigPromise = env.prod ? createProdBrowserConfig(baseProdBrowserConfig, babelConfig, envVars) : createDevBrowserConfig(baseDevBrowserConfig, babelConfig, envVars); const nodeConfigPromise = createNodeServerRenderingConfig( baseNodeServerRenderingConfig, babelConfig, ); const [browserConfig, nodeConfig] = await Promise.all([ browserConfigPromise, nodeConfigPromise, ]); const nodeServerRenderingConfig = { ...nodeConfig, mode: env.prod ? 'production' : 'development', }; const workersConfig = { ...baseWebWorkersConfig, plugins: env.prod ? prodWebWorkersPlugins : devWebWorkersPlugins, }; const webWorkersConfig = createWebWorkersConfig( env, workersConfig, babelConfig, ); return [browserConfig, nodeServerRenderingConfig, webWorkersConfig]; };